import Cesium, { Cartesian2, PixelDatatype, PixelFormat } from '../Ces/Cesium'
import WeFramebuffer from '../Ces/WeFramebuffer'

import WeDisplay from './WeDisplay'
import WeDisplayShaders from './WeDisplayShaders'

function createTexture(context, image, options) {
  let texture

  if (Cesium.defined(image.source)) {
    texture = new Cesium.Texture({
      context,
      pixelFormat: image.source.pixelFormat,
      pixelDatatype: image.source.pixelDatatype || image.source.pixelType,
      source: image.source,
      flipY: image.source.flipY ?? false,
      ...options,
    })

    texture.bytesLength = image.source.arrayBufferView.length
  } else {
    texture = new Cesium.Texture({
      context,
      source: image,
      ...options,
    })
  }

  texture.url = image.url

  return texture
}

export type WeTextureUniformOptions = {
  sampler?: any//Cesium.Sampler
  animation?: boolean
  calculateNormal?: boolean
  display?: WeDisplay
}

class WeTextureUniform {
  private _baseTexture: any// Cesium.Texture
  _lastBaseTexture: any//Cesium.Texture

  private _baseNormalFramebuffer: WeFramebuffer
  private _lastBaseNormalFramebuffer: WeFramebuffer

  private _animation: boolean
  private _textureResolution: Cartesian2
  private dirty: boolean = false
  private _image1: any
  private _image2: any
  private _options: any
  private _sampler: any//Cesium.Sampler
  private _ready: boolean = false
  private imageOpitons: {
    depth: number
  } = null

  private calculateNormal: boolean
  private copyCommand: any//Cesium.ComputeCommand
  private currentLayer: number = 0

  pixelFormat: PixelFormat
  pixelDatatype: PixelDatatype
  private display: WeDisplay
  constructor(options: WeTextureUniformOptions) {
    this._sampler = options.sampler
    this.display = options.display
    this._animation = options.animation ?? true
    this.calculateNormal = options.calculateNormal ?? false
    this._textureResolution = new Cesium.Cartesian2(512, 512)
  }

  get isCommitting() {
    return this.dirty
  }

  get ready() {
    return this._ready
  }

  get sampler() {
    return this._sampler
  }

  set sampler(value) {
    this._sampler = value
  }

  get baseTexture() {
    return this.baseNormalTexture
  }

  get lastBaseTexture() {
    return this._lastBaseTexture || this._baseTexture
  }

  get baseNormalTexture() {
    return this._baseNormalFramebuffer?.getColorTexture(0) || this._baseTexture
  }

  get lastBaseNormalTexture() {
    return this._lastBaseNormalFramebuffer?.getColorTexture(0) || this._baseTexture
  }

  get animation() {
    return this._animation
  }

  get textureResolution() {
    return this._textureResolution
  }

  pushDefines(defines) {
    if (this.calculateNormal && this.pixelDatatype === Cesium.PixelDatatype.UNSIGNED_BYTE) {
      defines.push(WeDisplayShaders.WeDefineCalculateNormal3D)
    }
  }

  updateData(context, image1, image2 = null, options = {}) {
    const source = image1.source || image1
    if (!this.imageOpitons) {
      this.imageOpitons = {
        depth: source.depth ?? 1,
      }

      this.pixelFormat = source.pixelFormat
      this.pixelDatatype = source.pixelDatatype
    }

    this._image1 = image1
    this._image2 = image2
    this._options = options

    if (this.display)
      this.display.imageryLayer.submitRef++

    this.dirty = true
  }

  submit(context) {
    const image1 = this._image1
    const image2 = this._image2
    const options = this._options

    this._image1 = undefined
    this._image2 = undefined

    const updateTexture = (texture, image) => {
      texture.copyFrom(image.source ? image : { source: image })
      texture.url = image.url
      return texture
    }

    if (Cesium.defined(image2)) {
      if (!Cesium.defined(this._lastBaseTexture)) {
        this._lastBaseTexture = createTexture(context, image2, options)
        console.debug('create _lastBaseTexture')
      } else if (image2.url !== this._lastBaseTexture.url) {
        if (Cesium.defined(this._baseTexture) && this._baseTexture.url === image2.url) {
          const temp = this._lastBaseTexture
          this._lastBaseTexture = this._baseTexture
          this._baseTexture = temp
        } else {
          updateTexture(this._lastBaseTexture, image2)
        }
      }
    }

    if (Cesium.defined(image1)) {
      if (!Cesium.defined(this._baseTexture)) {
        this._baseTexture = createTexture(context, image1, {
          sampler: this.sampler,
          ...options,
        })
        this.textureResolution.x = this._baseTexture.width
        this.textureResolution.y = this._baseTexture.height
        this._ready = true
      } else if (image1.url !== this._baseTexture.url) {
        if (Cesium.defined(this._lastBaseTexture) && this._lastBaseTexture.url === image1.url) {
          const temp = this._lastBaseTexture

          this._lastBaseTexture = this._baseTexture
          this._baseTexture = temp
        } else {
          updateTexture(this._baseTexture, image1)
        }
      }
    }

    if (this.display)
      this.display.imageryLayer.submitRef--
  }


  setupUniform(uniforms) {
    uniforms.u_textureResolution = this.textureResolution
    uniforms.u_BaseTexture = this.baseTexture
    if (this._animation) {
      uniforms.u_LastBaseTexture = this.lastBaseTexture
    }
    if (this.calculateNormal) {
      uniforms.u_BaseNormalTexture = this.baseNormalTexture
      if (this._animation) {
        uniforms.u_LastBaseNormalTexture = this.lastBaseNormalTexture
      }
    }
  }

  setupUniformMap(uniformMap) {
    uniformMap.u_textureResolution = () => this.textureResolution
    uniformMap.u_BaseTexture = () => this.baseTexture
    if (this._animation) {
      uniformMap.u_LastBaseTexture = () => this.lastBaseTexture
    }
  }

  get isSampler3D() {
    return this.imageOpitons.depth > 1
  }

  getSamplerDelcare() {
    const { isSampler3D, animation } = this
    const samplerUniform = isSampler3D ? 'sampler3D' : 'sampler2D'
    const samplerAnimation =
      animation ? `uniform ${samplerUniform} u_LastBaseTexture;\n` : ''

    return `
      precision highp ${samplerUniform};\n
      precision highp float;\n
      uniform ${samplerUniform} u_BaseTexture;\n
      ${samplerAnimation}
      uniform vec2 u_textureResolution;
    `
  }

  setLinear(): boolean {
    if (this._baseTexture) {
      if (
        this._baseTexture.sampler.magnificationFilter !== Cesium.TextureMagnificationFilter.LINEAR
      ) {
        this._baseTexture.sampler = Cesium.Sampler.LINEAR
        if (this._lastBaseTexture) {
          this._lastBaseTexture.sampler = Cesium.Sampler.LINEAR
        }
        return true
      }
    }
    return false
  }

  setNearest(): void {
    if (this._baseTexture) {
      this._baseTexture.sampler = Cesium.Sampler.NEAREST
    }
    if (this._lastBaseTexture) {
      this._lastBaseTexture.sampler = Cesium.Sampler.NEAREST
    }
  }

  uptateNormal(frameState) {
    const { depth } = this.imageOpitons
    if (!this.calculateNormal || depth <= 1) return
    const baseTexture = this._baseTexture
    if (!baseTexture || baseTexture.pixelDatatype !== Cesium.PixelDatatype.UNSIGNED_BYTE) return
    if (!this._baseNormalFramebuffer) {
      const context = frameState.context
      this._baseNormalFramebuffer = new WeFramebuffer({
        context,
        colorTextures: [
          new Cesium.Texture({
            context,
            width: baseTexture.width,
            height: baseTexture.height,
            depth: baseTexture.depth,
            pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
            pixelFormat: Cesium.PixelFormat.RGBA,
          })
        ]
      })

      const { display } = this

      const resolution = new Cesium.Cartesian3(
        baseTexture.width, baseTexture.height, depth
      )
      const uniformMap = {
        source: () => baseTexture,
        u_textureResolution: () => resolution,
        currentLayer: () => {
          return this.currentLayer / (depth - 1)
        },
      }

      display.valueDecoder.weUniform.setupUniformMap(uniformMap)

      const textureValueFunc = baseTexture.pixelFormat === Cesium.PixelFormat.ALPHA ?
        'color.a' : 'color.r'

      const fragmentCopy = `
        precision highp sampler2D;
        precision highp sampler3D;
        uniform sampler3D source;
        uniform vec3 u_textureResolution;
        uniform float currentLayer;
        in vec2 v_textureCoordinates;
        ${display.valueDecoder.weUniform.getSource(true)}

        float toSNorm(float value, float rangeMaximum)
        {
          return round(
            (clamp(value, -1.0f, 1.0f) * 0.5 + 0.5) * rangeMaximum
          );
        }

        vec2 octEncodeInRange(vec3 normal, float rangeMax)
        {
          vec2 result;
          result.x =
            normal.x / (abs(normal.x) + abs(normal.y) + abs(normal.z));
          result.y =
            normal.y / (abs(normal.x) + abs(normal.y) + abs(normal.z));

          if (normal.z < 0.0) {
            float x = result.x;
            float y = result.y;
            result.x = (1.0 - abs(y)) * czm_signNotZero(x);
            result.y = (1.0 - abs(x)) * czm_signNotZero(y);
          }

          result.x = toSNorm(result.x, rangeMax);
          result.y = toSNorm(result.y, rangeMax);

          return result;
        }

        vec2 octEncode(vec3 vector) {
          return octEncodeInRange(vector, 255.0);
        }

        vec4 decodeValue(vec3 str) {
            vec4 color = texture(source, str);
            float offset = Weget_offset();
            float scale = Weget_scale();
            float value = czm_valueTransform(offset,scale,${textureValueFunc});
            return vec4(color.x,value,0.0,color.a);
        }

        vec4 decodeValue(vec2 uv) {
            return decodeValue(vec3(uv,currentLayer));
        }

        void main(void){
            vec2 uv = v_textureCoordinates;
            vec4 comp = decodeValue(uv);
            vec3 d = 1.0 / (u_textureResolution - 1.0);

            float value = comp.g;

            vec3 normal = vec3(-1.0);
            if(uv.x > d.x
            && uv.x < 1.0 - d.x
            && uv.y > d.y
            && uv.y < 1.0 - d.y
            && currentLayer < 1.0 - d.z * 0.1
            && currentLayer > d.z)
            {
              float valueW = decodeValue(uv + vec2(d.x,0.0)).g;
              float valueE = decodeValue(uv - vec2(d.x,0.0)).g;
              float valueN = decodeValue(uv + vec2(0.0,d.y)).g;
              float valueS = decodeValue(uv - vec2(0.0,d.y)).g;

              float valueT = decodeValue(vec3(uv,currentLayer + d.z)).g;
              float valueB = decodeValue(vec3(uv,currentLayer - d.z)).g;

              float dx = min(valueW, valueE) - value;
              float dy = min(valueS, valueN) - value;
              float dz = max(valueT, valueB) - value;
              normal = vec3(dx,dy,dz);
            }

            vec2 octNor = octEncode(normalize(normal)) / 255.0;
            vec4 color = vec4(comp.x,octNor,comp.w);
            out_FragColor = color;
        }
      `
      this.copyCommand = new Cesium.ComputeCommand({
        shaderProgram: Cesium.ShaderProgram.fromCache({
          context,
          vertexShaderSource: Cesium._shadersViewportQuadVS,
          fragmentShaderSource: `
          ${fragmentCopy}
          `,
        }),
        uniformMap,
        preExecute: () => {
          this._baseNormalFramebuffer.currentLayer = this.currentLayer
        },
        postExecute: () => {
          this.currentLayer++
        },
        persists: true,
        owner: this,
      })
      this.copyCommand.framebuffer = this._baseNormalFramebuffer
    }

    this.currentLayer = 0
    for (let i = 0; i < depth; i++) {
      frameState.commandList.push(this.copyCommand)
    }
  }

  update(frameState) {
    if (!frameState.passes.render || frameState.loadingRef > 0) return
    if (this.dirty) {
      this.dirty = false
      this.submit(frameState.context)
      this.uptateNormal(frameState)
      if (this.display) {
        this.display.dirty = true
      }
    }
  }

  isDestroyed() {
    return false
  }

  destroy() {
    this._baseTexture = this._baseTexture?.destroy()
    this._lastBaseTexture = this._lastBaseTexture?.destroy()

    this._baseNormalFramebuffer = this._baseNormalFramebuffer?.destroy()
    this._lastBaseNormalFramebuffer = this._lastBaseNormalFramebuffer?.destroy()

    this.copyCommand = this.copyCommand?.shaderProgram?.destroy()

    return Cesium.destroyObject(this)
  }
}

export default WeTextureUniform
